# -*- coding: utf8 -*-

from StringIO import StringIO
from xml.etree.ElementTree import XML
from xml.etree.ElementTree import iterparse

OPENSEARCH_VERSION = "http://a9.com/-/spec/opensearch/1.1/"

class Description(object):
  """
  OpenSearch Description.
  
  Models an OpenSearch Description document. 
  
  See: http://www.opensearch.org/Specifications/OpenSearch/1.1#OpenSearch_description_document
  
  """
  
  def __init__(self):
    """Constructor for Description."""
    super(Description, self).__init__()
    self.short_name = ''
    self.long_name = ''
    self.description = ''
    self.tags = [] # e.g. <Tags>example web</Tags>
    self.conact = ''
    self.urls = [] # e.g. <Url type="application/rss+xml" template="http://example.com/?q={searchTerms}&amp;pw={startPage?}&amp;format=rss"/>
    self.image = [] # e.g. <Image height="64" width="64" type="image/png">http://example.com/websearch.png</Image>
    self.query = {} # e.g. <Query role="example" searchTerms="cat" />
    self.developer = ''
    self.attribution = ''
    self.syndication_right = ''
    self.adult_content = ''
    self.language = ''
    self.output_encoding = ''
    self.input_encoding = ''
    
  
  
  def write_xml(self):
    """
    Creates XML for an OpenSearch Description document.
    
    Returns:
    
    * description_xml (str): OpenSearch Description document as XML
    
    **TODO code Description.write_xml()**
    
    """
    pass
  
  
  @staticmethod
  def parse_xml(self, oss_xml):
    """
    Parse OpenSearch Description XML.
    
    Parameters:
      
    * oss_xml (str): OpenSearch Description XML
    
    Returns:
      
    * puppy.model.OpenSearch.Description
    
    TODO code Description.parse_xml()
    
    """
    pass
  


class Query(object):
  """
  OpenSearch Query.
  
  Models an OpenSearch Query element. 
  
  See: http://www.opensearch.org/Specifications/OpenSearch/1.1#OpenSearch_Query_element
  
  """
  
  def __init__(self, search_terms):
    """
    Constructor for Query.
    
    Parameters:
    
    * search_terms (str): the search terms of the query
    
    """
    super(Query, self).__init__()
    self.search_terms = search_terms
    self.count = 0
    self.start_index = 0
    self.start_page = 0
    self.language = ''
    self.service = ''
    self.suggestions = {}
    
  def __str__(self):
    return u"Query:\n\tterms: {0}\n\tsuggestions: {1}".format(self.search_terms, self.suggestions)
    
    
  def write_xml(self):
    """
    Creates XML for OpenSearch Query.
    
    Returns:
    
    * query_xml (str): OpenSearch Query as XML
    
    TODO code Query.write_xml()
    
    """
    pass
  
  
  @staticmethod
  def parse_xml(self, oss_xml):
    """
    Parse OpenSearch Query XML.
    
    Parameters:
    
    * oss_xml (str): OpenSearch Query XML
    
    Returns:
    
    * puppy.model.OpenSearch.Query
    
    TODO code Query.parse_xml()
    
    """
    pass
  


class Response(object):
  """
  OpenSearch Response.
  
  Models an OpenSearch Response element.  
  
  See: http://www.opensearch.org/Specifications/OpenSearch/1.1#OpenSearch_response_elements
  
  """
  
  def __init__(self):
    """Constructor for Response."""
    super(Response, self).__init__()
    self.title = 'n/a'
    self.description = 'n/a'
    self.total_results = 0
    self.start_index = 0
    self.items_per_page = 0
    self.query = 'n/a'
    self.items = []
    self.namespaces = { }
  
  
  def __str__(self):
    return u"Response:\n\ttitle: {0}\n\tdescription: {1}\n\ttotal results: {2}\n\tstart page: {3}\n\titems per page: {4}\n\tquery: {5}\n\titems: {6}".format(self.title, self.description, self.total_results, self.start_index, self.items_per_page, self.query, len(self.items))
  
  
  def write_rss(self):
    """
    Creates an RSS feed from a OpenSearch Response.
    
    Returns:
      
    * response_xml (str): OpenSearch Response as XML
    """
    xml = u'<?xml version="1.0" encoding="UTF-8"?>'
    rss_open = u'<rss version="2.0" {0}>'.format(" ".join(
      ['xmlns:{0}="{1}"'.format(self.namespaces[ns_key], ns_key) 
        for ns_key in self.namespaces.keys()]))
    channel_open = '<channel>'
    title = '<title>{0}</title>'.format(self.title)
    link = '<link>{0}</link>'.format("*** missing from Response ***")
    description = u'<description>Search results for {0}</description>'.format(self.description)
    if self.namespaces.has_key(OPENSEARCH_VERSION):
      oss_prefix = self.namespaces[OPENSEARCH_VERSION]
    else:
      oss_prefix = 'opensearch'
    oss_total_results = u'<{0}:totalResults>{1}</{0}:totalResults>'.format(oss_prefix, self.total_results)
    oss_start_index = u'<{0}:startIndex>{1}</{0}:startIndex>'.format(oss_prefix, self.start_index)
    oss_items_per_page = u'<{0}:itemsPerPage>{1}</{0}:itemsPerPage>'.format(oss_prefix, self.items_per_page)
    oss_query = u'<{0}:Query role="request" searchTerms="{1}" startPage="{2}" />'.format(oss_prefix, self.query, self.start_index)
    items = u''
    for item in self.items:
      items = u''.join([items, item.write_rss()])
    channel_close = u'</channel>'
    rss_close = u'</rss>'
    response_xml = (u"".join([xml, rss_open, channel_open, title, link, description, oss_total_results, oss_start_index, oss_items_per_page, oss_query, items, channel_close, rss_close])).encode("utf-8")
    return response_xml

  def write_xml(self):
    """
    Creates an XML from a OpenSearch Response.
    For web

    Returns:

    * response_xml (str): OpenSearch Response as XML
    """
    xml = u'<?xml version="1.0" encoding="UTF-8"?>\n'
    feed_open = u'<feed xmlns="http://www.w3.org/2005/Atom" xmlns:opensearch="http://a9.com/-/spec/opensearch/1.1/">\n'
    title = u'<title>{0}</title>\n'.format(self.title)
    link = u'<link>{0}</link>\n'.format("*** missing from Response ***")
    description = u'<description>Search results for {0}</description>\n'.format(self.description)
    oss_prefix = 'opensearch'
    oss_total_results = u'<{0}:totalResults>{1}</{0}:totalResults>\n'.format(oss_prefix, self.total_results)
    oss_start_index = u'<{0}:startIndex>{1}</{0}:startIndex>\n'.format(oss_prefix, self.start_index)
    oss_items_per_page = u'<{0}:itemsPerPage>{1}</{0}:itemsPerPage>\n'.format(oss_prefix, self.items_per_page)
    oss_query = u'<{0}:Query role="request" searchTerms="{1}" startPage="{2}" />\n'.format(oss_prefix, self.query, self.start_index)
    items = u''
    for item in self.items:
      items = u''.join([items, item.write_xml()])
    feed_close = u'</feed>\n'
    response_xml = (u"".join([xml, feed_open, title, link, description, oss_total_results, oss_start_index, oss_items_per_page, oss_query, items, feed_close])).encode("utf-8")
    return response_xml

  
  @staticmethod
  def parse_xml(oss_xml):
    """
    Parse OpenSearch Response XML.
    
    Parameters:
    
    * oss_xml (str): OpenSearch Response XML
    
    Returns:
    
    * response (puppy.model.OpenSearch.Response)
    
    """
    
    def get_namespaces(response, source):
      """
      Builds a list of (prefix, uri) namespaces.
      
      Preserves the orginal namespaces that ElementTree discards when parsing XML documents.  
      
      Parameters:
      
      * source (str): OpenSearch Response XML
      
      Returns:
      
      * namespaces (dict): contains namespace_uri : namespace pairs
      
      """
      namespaces = {}
      events = ("end", "start-ns", "end-ns")
      for event, elem in iterparse(StringIO(source), events=events):
        if event == "start-ns":
          namespaces[elem[1]] = elem[0]
      return namespaces
    
    
    oss_root = XML(oss_xml)
    oss_channel = oss_root.find('channel')
    response = Response( )
    response.namespaces = get_namespaces(response, oss_xml)
    response.title = oss_channel.find('title').text
    response.description = oss_channel.find('description').text
    response.total_results = oss_channel.find('{http://a9.com/-/spec/opensearch/1.1/}totalResults').text
    response.start_index = oss_channel.find('{http://a9.com/-/spec/opensearch/1.1/}startIndex').text
    response.items_per_page = oss_channel.find('{http://a9.com/-/spec/opensearch/1.1/}itemsPerPage').text
    response.query = oss_channel.find('{http://a9.com/-/spec/opensearch/1.1/}Query').attrib['searchTerms']
    for oss_item in oss_channel.findall('item'):
      response.items.append(Item.parse_item_xml(oss_item, response.namespaces))
    return response
  


class Item(object):
  """
  Item from `Response`.
  
  Each :py:class:`Response` contains a <channel> that has 0 or more <item> elements.  At their most basic, Items have the title, link and a description that describes a single search result.  However, additional elements can be added (e.g. <dc:book>) that need to be stored.
  
  """
  def __init__(self):
    """
    Constructor for Item
    
    TODO convert self.elements to list.  There may be duplicate elements (e.g. <author>alice</author> <author>bob</author>), and currently only the last one is remembered.
    
    """
    super(Item, self).__init__()
    self.title = ''
    self.link = ''
    self.description = ''
    self.elements = {}
    self.namespaces = {}
  
  
  def __str__(self):
    return u'Item:\n\ttitle: {0}\n\tlink: {1}\n\tdescription: {2}\n\telements: {3}'.format(self.title, self.link, self.description, len(self.elements))
  
  
  def write_rss(self):
    """
    Create XML for Item.
    
    Returns:
    
    * item_xml (str): OpenSearch Response Item as XML
      
    """
    item_open = u'<item>'
    title = u'<title>{0}</title>'.format(self.title)
    link = u'<link>{0}</link>'.format(self.link)
    description = u'<description>{0}</description>'.format(self.description)
    elements = u''
    for tag, content in self.elements.items( ):
      attributes = u''
      for attrib in content['attributes']:
        key, val = attrib.items( )[0]
        attrib_str = u'{0}="{1}"'.format(key, val)
        attributes = u' '.join([attributes, attrib_str])
      if tag[0] == u'{':
        uri, prefix = tag.split('}')
        tag = u':'.join([self.namespaces[uri[1:]], prefix])
      element = u'<{0}{1}>{2}</{0}>'.format(tag, attributes, content['text'])
      elements = u''.join([elements, element])
    item_close = u'</item>'
    item_xml = ''.join([item_open, title, link, description, elements, item_close])
    return item_xml
 
  def write_xml(self):
    """
    Create XML for Item.
    
    Returns:
    
    * item_xml (str): OpenSearch Response Item as XML
      
    """
    item_open = u'<entry>'
    title = u'<title>{0}</title>'.format(self.title)
    link = u'<link>{0}</link>'.format(self.link)
    description = u'<content type="text">{0}</content>'.format(self.description)
    elements = u''
    for tag, content in self.elements.items( ):
      attributes = u''
      for attrib in content['attributes']:
        key, val = attrib.items( )[0]
        attrib_str = u'{0}="{1}"'.format(key, val)
        attributes = u' '.join([attributes, attrib_str])
      if tag[0] == u'{':
        uri, prefix = tag.split('}')
        tag = u':'.join([self.namespaces[uri[1:]], prefix])
      element = u'<{0}{1}>{2}</{0}>'.format(tag, attributes, content['text'])
      elements = u''.join([elements, element])
    item_close = u'</entry>\n'
    item_xml = ''.join([item_open, title, link, description, elements, item_close])
    return item_xml
  
  @staticmethod
  def parse_item_xml(item_xml, namespaces):
    """Parse Item from an ElementTree element.
    
    Parameters:
    
    * item_xml (str): item element xml
    * namespaces (dict): all of the namespaces discovered in Response during parsing
    
    Returns:
    
    * item (puppy.model.OpenSearch.Item)
    
    TODO WARNING - DOES NOT SUPPORT DUPLICATE ELEMENTS
    
    """
    item = Item( )
    item.namespaces = namespaces
    for elem in list(item_xml):
      # opensearch elements
      if elem.tag == 'title':
        item.title = elem.text
      elif elem.tag == 'link':
        item.link = elem.text
      elif elem.tag == 'description':
        item.description = elem.text
      else:
        item.elements[elem.tag] = {'attributes': [], 'text': ''}
        if elem.keys( ):
          # handle attributes
          for key in elem.keys( ):
            item.elements[elem.tag]['attributes'].append({key: elem.attrib[key]})
        if elem.text is None:
          # handle no text content
          item.elements[elem.tag]['text'] = ''
        elif elem.text.isspace():
          # handle whitespace text content
          item.elements[elem.tag]['text'] = ''
        else:
          # handle text content
          item.elements[elem.tag]['text'] = elem.text.strip()
    return item

  
